using System.Windows.Threading;
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Data.Linq;
using System.Data.SqlClient;
using System.Linq;
using PDO.DataPersistence.ServiceProvider;
using PDO.Utilities;

namespace PDO.DataPersistence.Core
{
    /// <summary>
    /// Summary description for DataSession.
    /// </summary>
    public class DataSession
    {

        #region Enumerations

        public enum DatabaseSelector
        {
            Automatic = 0,
            Local = 1,
            Central = 2
        }

        #endregion

        #region Private Members and Properties

        private static object _syncObject = new object();

        private static DataSession _defaultSession;

        private DatabaseSelector _selectedDatabase;

        private Guid _userGuid;

        private int _pageSize;

        private static IDataObjectSourceProvider _dataObjectSourceProvider;


        // Used only as a work-around to allow SPEDGen generated table classes to work
        // with LINQ. We use LINQ to create sql statements that are extracted from the linq 
        // expression and passed through our own data layer.
        private static DataContext _linqBufferDataContext;

        public static DataContext LINQDataContext
        {
            get
            {
                if (_linqBufferDataContext == null)
                {
                    // NOTE: LINQ cannot generate native sql statements without a valid, live connection to target database.
                    // This makes using LINQ querys impossible when working with "disconnected" data in a true N-Tier architecture!
                    // However, the connection only needs to be to a valid data source; it doesn't need to be in any way related to 
                    // our SPEDgen tables!

                    // Create a DataContext based on a completely empty database 
                    _linqBufferDataContext = GetNewLinqDataContext();

                }
                return _linqBufferDataContext;
            }
        }

        public static DataContext GetNewLinqDataContext()
        {
            return new DataContext("LINQBuffer.sdf");  // sdf file must be in same directory as assembly
        }


        private DataRequest.DatabaseTarget RequestedTargetDatabase()
        {
            DataRequest.DatabaseTarget retValue;

            switch (SelectedDatabase)
            {
                case DatabaseSelector.Local:
                    retValue = DataRequest.DatabaseTarget.Local;
                    break;

                case DatabaseSelector.Central:
                    retValue = DataRequest.DatabaseTarget.Central;
                    break;

                case DatabaseSelector.Automatic:
                    retValue = DataRequest.DatabaseTarget.Automatic;
                    break;

                default:
                    throw new ConstraintException("Invalid value for SelectedDatabase");
            }
            return retValue;
        }


        #endregion

        public delegate void DataSessionCallbackDelegate();
        private static DataSessionCallbackDelegate _updateCallback; // Under development - may be deprecated.

        #region Public Static

        /// <summary>
        /// Synchronizes access to the DataObjectSourceProvider property.
        /// </summary>
        public static Object SyncObject
        {
            get
            {
                return _syncObject;
            }
        }

        public static void SetDataSourceProvider(IDataObjectSourceProvider provider)
        {
            SetDataSourceProvider(provider, null);
        }


        public static DataSession DefaultSession
        {
            get
            {
                if (_defaultSession == null)
                {
                    _defaultSession = new DataSession();
                }
                return _defaultSession;
            }
            set
            {
                _defaultSession = value;
            }
        }

        public static void SetDataSourceProvider(IDataObjectSourceProvider provider, DataSessionCallbackDelegate callback)
        {
            using (TimedLock.Lock(_syncObject))
            {
                /* 
                 * if (DataSession.DefaultSession != null)
                 * {
                 *  if (DataSession.DataObjectSourceProvider != null)
                 *  {                    
                 *      // DO NOT DISPOSE UNLESS NO OTHER OPERATIONS ARE GOING ON
                 *      DataSession.DataObjectSourceProvider.Dispose();                    
                 *  }
                 * }
                 */

                if (_dataObjectSourceProvider != null)
                {
                    _dataObjectSourceProvider.Dispose();
                }

                _dataObjectSourceProvider = provider;
            }
        }

        #endregion


        #region Public Properties and Methods

        public DatabaseSelector SelectedDatabase
        {
            get { return _selectedDatabase; }
        }


        public DataSession()
        {
            _selectedDatabase = DatabaseSelector.Automatic;
            _pageSize = 100;
        }


        // Set DatabaseSelector only via constructor to minimize chance of inadvertant change.
        public DataSession(DatabaseSelector selectedDatabase)
        {
            _selectedDatabase = selectedDatabase;
        }

        public Guid UserGuid
        {
            get { return _userGuid; }
            set { _userGuid = value; }
        }

        public bool TestConnection()
        {
            using (TimedLock.Lock(_syncObject))
            {
                return DataObjectSourceProvider.TestConnection();
            }
        }

        public int DefaultPageSize
        {
            get { return _pageSize; }
            set { _pageSize = value; }
        }

        #endregion

        #region Transaction Support

        public void BeginTransaction()
        {
            //??DataObjectSourceProvider.BeginTransaction();
        }

        public void CommitTransaction()
        {
            //??DataObjectSourceProvider.CommitTransaction();
        }

        public void RollbackTransaction()
        {
            //??DataObjectSourceProvider.RollbackTransaction();
        }
        
        #endregion

        #region Database persistance methods

        /// <summary>
        /// 
        /// </summary>
        static public IDataObjectSourceProvider DataObjectSourceProvider
        {
            get
            {
                if (null == _dataObjectSourceProvider)
                {
                    throw new InvalidOperationException("Data source provider not properly initialized.");
                }

                return _dataObjectSourceProvider;
            }

        }

        //public DataSet FillEtherealDataSet(DataRequest cachedDataRequest)
        //{
        //    return DataObjectSourceProvider.FillEtherealDataSet(cachedDataRequest);
        //}

        #region Cached interface

        /// <summary>
        /// Create a database entry for provided PersistableDataObject.
        /// </summary>
        /// <param name="pdoType">PersistableDataObject Type - may be base or derived Type</param>
        /// <param name="pdo">PersistableDataObject</param>
        /// <param name="parameterValues">pdo property values to save</param>
        /// <returns></returns>
        public object Create(Type pdoType, PersistableDataObject pdo, params object[] parameterValues)
        {
            Debug.Assert(pdo != null);

            //if (_updateCallback != null)
            //{
            //    _updateCallback();
            //}

            string spName = PDOMapper.GetProcName(pdoType, PDOMapper.SProcOperations.Create);
            object result = ExecuteScalar(PDOMapper.GetListUpdateNotificationInfo(pdo), pdo.IsEthereal, spName,
                OperationType.Create, parameterValues);

            //SaveAuditInfo(pdoType, pdo, OperationType.Create);

            return result;
        }


        //DEPRECATED
        //public bool Read(PersistableDataObject pdo, string spName, object primaryKey)
        //{
        //    Debug.Assert(pdo != null);

        //    bool foundInCache = false;
        //    DataSet ds;

        //    // Build key string using requested primary key instead of pdo.DBKey 
        //    // since pdo.DBKey is not set until object is successfully loaded.
        //    string key = pdo.GetDBKey(primaryKey);

        //    //if (CacheManager.Contains(key))
        //    //{
        //    //    ds = CacheManager.Get(key);
        //    //    foundInCache = true;
        //    //}
        //    //else
        //    //{
        //    ds = ExecuteDataset(PDOMapper.GetListUpdateNotificationInfo(pdo), pdo.IsEthereal, spName, primaryKey);
        //    //}

        //    if (ds.Tables[0].Rows.Count > 1)
        //    {
        //        // Primary key problem - should not happen with proper constraints on database.
        //        throw new PersistableDataObjectException("Multiple records with the same primary key were found in " + pdo.SourceDatabaseTableName);
        //    }

        //    if (ds.Tables[0].Rows.Count < 1)
        //    {
        //        // Requested record not found.
        //        pdo.IsNew = true;
        //        return false;
        //    }

        //    pdo.LoadFromDataRow(ds.Tables[0].Rows[0]);

        //    //if (!foundInCache)
        //    //{
        //    //    CacheManager.Put(key, ds);
        //    //}

        //    return true;
        //}



        static bool _cachingEnabled = false; // TEMP - Disabled by default. Once we get sync fully functional, turn it on.

        /// <summary>
        /// Read PersistableDataObject values from database.
        /// </summary>
        /// <param name="pdoType">PersistableDataObject Type - may be base or derived Type</param>
        /// <param name="pdo">PersistableDataObject</param>
        /// <param name="primaryKey"></param>
        /// <returns></returns>
        public bool Read(Type pdoType, PersistableDataObject pdo, object primaryKey)
        {
            Debug.Assert(pdo != null);

            string spName = PDOMapper.GetProcName(pdoType, PDOMapper.SProcOperations.Read);

            bool foundInCache = false;
            DataSet ds;

            using (TimedLock.Lock(pdo.SyncRoot))
            {
                // Build key string using requested primary key instead of pdo.DBKey 
                // since pdo.DBKey is not set until object is successfully loaded.
                string key = PDOMapper.GetDBKey(pdo, primaryKey);

                if (_cachingEnabled && CacheManager.Contains(key))
                {
                    ds = CacheManager.Get(key);
                    foundInCache = true;
                }
                else
                {
                    ds = ExecuteDataset(PDOMapper.GetItemUpdateNotificationInfo(pdo), pdo.IsEthereal, spName, primaryKey);
                }

                if (ds.Tables[0].Rows.Count > 1)
                {
                    // Primary key problem - should not happen with proper constraints on database.
                    throw new PersistableDataObjectException("Multiple records with the same primary key were found in " + pdo.SourceDatabaseTableName);
                }

                if (ds.Tables[0].Rows.Count < 1)
                {
                    // Requested record not found.
                    if (!pdo.IsNew)
                    {
                        throw new PersistableDataObjectException("Failed to re-fresh an existing object from table " + pdo.SourceDatabaseTableName);
                    }
                    return false;
                }

                pdo.PrepareLoad(); // TEMPORARY - move to generated class Load method.

                pdo.LoadFromDataRow(ds.Tables[0].Rows[0]);

                if (_cachingEnabled && !foundInCache)
                {
                    CacheManager.Put(key, ds);
                }
            }

            //SaveAuditInfo(pdoType, pdo, OperationType.Read);

            return true;
        }

        /// <summary>
        /// Update existing database values for provided PersistableDataObject.
        /// </summary>
        /// <param name="pdoType">PersistableDataObject Type - may be base or derived Type</param>
        /// <param name="pdo">PersistableDataObject</param>
        /// <param name="parameterValues">pdo property values to save</param>
        public void Update(Type pdoType, PersistableDataObject pdo, params object[] parameterValues)
        {
            Debug.Assert(pdo != null);


            string spName = PDOMapper.GetProcName(pdoType, PDOMapper.SProcOperations.Update);
            int retval = ExecuteNonQuery(PDOMapper.GetItemUpdateNotificationInfo(pdo), pdo.IsEthereal,
                spName, OperationType.Update, parameterValues);

            // For now, just clear item from cache to force a re-load.
            if (CacheManager.Contains(pdo.DBKey))
            {
                CacheManager.Remove(pdo.DBKey);
            }

            //SaveAuditInfo(pdoType, pdo, OperationType.Update);
        }


        /// <summary>
        /// Delete existing database record for provided PersistableDataObject.
        /// </summary>
        /// <param name="pdoType">PersistableDataObject Type - may be base or derived Type</param>
        /// <param name="pdo">PersistableDataObject</param>
        /// <param name="primaryKey"></param>
        public void Delete(Type pdoType, PersistableDataObject pdo, object primaryKey)
        {
            Debug.Assert(pdo != null);

            pdo.MarkedForDeletion = true;

            string spName = PDOMapper.GetProcName(pdoType, PDOMapper.SProcOperations.Delete);

            int retval = ExecuteNonQuery(PDOMapper.GetListUpdateNotificationInfo(pdo), pdo.IsEthereal,
                spName, OperationType.Delete, primaryKey);

            if (CacheManager.Contains(pdo.DBKey))
            {
                CacheManager.Remove(pdo.DBKey);
            }

            //SaveAuditInfo(pdoType, pdo, OperationType.Delete);
        }


        public DataSet GetDataset(IPersistableDataObjectList pdoList, string spName, params object[] parameterValues)
        {
            DataSet ds;

            // CX Disable caching for now.
            //if (CacheManager.Contains(pdoList.DBKey))
            //{
            //    ds = CacheManager.Get(pdoList.DBKey);
            //}
            //else
            //{
            ds = ExecuteDataset(spName, parameterValues);

            //if ((null != ds) && (ds.Tables[0].Rows.Count < CacheManager.MaxCachedRows))
            //{
            //    CacheManager.Put(pdoList.DBKey, ds);
            //}
            //}

            return ds;
        }


        public DataSet GetDataset(IPersistableDataObjectList pdoList, CommandType command, string sql)
        {
            DataSet ds;

            // CX Disable caching for now.
            //if (CacheManager.Contains(pdoList.DBKey))
            //{
            //    ds = CacheManager.Get(pdoList.DBKey);
            //}
            //else
            //{
            ds = ExecuteDataset(command, sql);

            //if ((null != ds) && (ds.Tables[0].Rows.Count < CacheManager.MaxCachedRows))
            //{
            //    CacheManager.Put(pdoList.DBKey, ds);
            //}
            //}

            return ds;
        }

        public DataSet GetDataset(IPersistableDataObjectList pdoList, CommandType command, string sql, params object[] parameterValues)
        {
            DataSet ds;

            // CX Disable caching for now.
            //if (CacheManager.Contains(pdoList.DBKey))
            //{
            //    ds = CacheManager.Get(pdoList.DBKey);
            //}
            //else
            //{
            DataRequest dr = new DataRequest(DataRequest.DatabaseTarget.Automatic) { CommandType = CommandType.Text, Parameters = parameterValues.ToList<object>(), SqlCommandString = sql };
            ds = ExecuteDataset(dr);

            //if ((null != ds) && (ds.Tables[0].Rows.Count < CacheManager.MaxCachedRows))
            //{
            //    CacheManager.Put(pdoList.DBKey, ds);
            //}
            //}

            return ds;
        }

        #endregion Cached interface

        #region ExecuteScalar

        public object ExecuteScalar(NotificationMessage notificationInformation, bool ethereal, string spName, params object[] parameterValues)
        {
            return ExecuteScalar(notificationInformation, ethereal, spName, OperationType.Unknown, parameterValues);
        }


        public object ExecuteScalar(NotificationMessage notificationInformation, bool ethereal, string spName,
            OperationType operationType, params object[] parameterValues)
        {

            DataRequest dataRequest = new DataRequest(RequestedTargetDatabase())
            {
                SqlCommandString = spName,
                CommandType = CommandType.StoredProcedure,
                EtherealRequest = ethereal,
                NotificationMessage = notificationInformation,
                SqlOperationType = operationType,
                Parameters = new List<object>(parameterValues)
            };

            dataRequest.NotificationMessage.OperationType = operationType;
            return ExecuteScalar(dataRequest);
        }

        public object ExecuteScalar(DataRequest dataRequest)
        {
            using (TimedLock.Lock(_syncObject))
            {
                try
                {
                    return DataObjectSourceProvider.ExecuteScalar(dataRequest);
                }
                catch (DataObjectSourceException ex)
                {
                    throw new PersistableDataObjectException("Scalar Action Failed.", ex);
                }
            }
        }
        #endregion

        #region ExecuteDataSet

        public DataSet ExecuteDataset(string spName, params object[] parameterValues)
        {
            return ExecuteDataset(new NotificationMessage(), false, spName, parameterValues);
        }

        public DataSet ExecuteDataset(NotificationMessage notificationInformation, bool ethereal, string spName, params object[] parameterValues)
        {
            return ExecuteDataset(notificationInformation, ethereal, spName, OperationType.Unknown, parameterValues);
        }


        public DataSet ExecuteDataset(NotificationMessage notificationInformation, bool ethereal, string spName, OperationType operationType, params object[] parameterValues)
        {
            DataRequest dataRequest = new DataRequest(RequestedTargetDatabase())
            {
                SqlCommandString = spName,
                CommandType = CommandType.StoredProcedure,
                EtherealRequest = ethereal,
                NotificationMessage = notificationInformation,
                SqlOperationType = operationType
            };

            dataRequest.NotificationMessage.OperationType = operationType;

            if (null != parameterValues)
            {
                dataRequest.Parameters = new List<object>(parameterValues);
            }

            return ExecuteDataset(dataRequest);
        }

        public DataSet ExecuteDataset(CommandType command, string sql)
        {
            DataRequest dataRequest = new DataRequest(RequestedTargetDatabase())
            {
                SqlCommandString = sql,
                CommandType = command,
                EtherealRequest = false
            };
            return ExecuteDataset(dataRequest);
        }

        public DataSet ExecuteDataset(DataRequest dataRequest)
        {
            using (TimedLock.Lock(_syncObject))
            {
                try
                {
                    return DataObjectSourceProvider.ExecuteDataSet(dataRequest);
                }
                catch (DataObjectSourceException ex)
                {
                    throw new PersistableDataObjectException("Dataset Action Failed", ex);
                }
            }
        }

        #endregion

        #region ExecuteNonQuery

        public int ExecuteNonQuery(string spName, params object[] parameterValues)
        {
            return ExecuteNonQuery(new NotificationMessage(), false, spName, parameterValues);
        }
        public int ExecuteNonQuery(NotificationMessage notificationInformation, bool ethereal, string spName, params object[] parameterValues)
        {
            return ExecuteNonQuery(notificationInformation, ethereal, spName, OperationType.Unknown, parameterValues);
        }

        public int ExecuteNonQuery(NotificationMessage notificationInformation, bool ethereal, string spName, OperationType operationType, params object[] parameterValues)
        {
            DataRequest dataRequest = new DataRequest(RequestedTargetDatabase())
            {
                EtherealRequest = ethereal,
                SqlCommandString = spName,
                CommandType = CommandType.StoredProcedure,
                NotificationMessage = notificationInformation,
                SqlOperationType = operationType
            };

            dataRequest.NotificationMessage.OperationType = operationType;

            dataRequest.Parameters = new List<object>(parameterValues);

            return ExecuteNonQuery(dataRequest);

        }

        public int ExecuteNonQuery(DataRequest dataRequest)
        {
            using (TimedLock.Lock(_syncObject))
            {
                try
                {
                    return DataObjectSourceProvider.ExecuteNonQuery(dataRequest);
                }
                catch (DataObjectSourceException ex)
                {
                    throw new PersistableDataObjectException("NonQuery Action Failed", ex);
                }
            }
        }
        #endregion

        #endregion

    }


}